Zephyr 编写设备树文件

Zephyr 可以通过设备树文件描述芯片/板卡的外设。具体的设备模型、设备树、设备模型的说明参见官方文档:

https://docs.zephyrproject.org/latest/kernel/drivers/index.html

https://docs.zephyrproject.org/latest/build/dts/index.html

https://docs.zephyrproject.org/latest/hardware/porting/board_porting.html

本文描述的是如何针对已有的板卡(如 STM32 官方开发板 Nucleo F411RE,本文以此为例)通过添加 Overlay 文件进行改造。这是符合工程直觉和开发流程的做法。我们不会从 0 开始新建一个板卡,而是选择在现有的板卡的基础上进行改造。

关于 Overlay 文件在设备树中的位置,参见:Zephyr 工作逻辑。本文以添加 SPI 外设为例进行演示。

设备树绑定

Zephyr 中所有的设备树绑定文件都在 zephyr/dts/bindings/ 文件夹下。对于该板卡,其描述 SPI 的设备数绑定文件(.yaml)为 zephyr/dts/bindings/spi/st,stm32-spi.yaml我们编写的设备树节点必须满足该文件描述的规则。

可以通过安装 fzf 等工具,实现基于文件名的快速检索。如,搜索 st,stm32-spi 即可快速定位到文件位置,在终端中利用 Ctrl + Click 即可快速打开。如下图所示。

该文件的内容如下:

description: STM32 SPI controller

compatible: "st,stm32-spi"

include: st,stm32-spi-common.yaml

其中 description 字段描述了该绑定文件的作用对象,即 STM32 的 SPI 外设控制器。compatible 字段为身份标识,任何 stm32 spi 外设均需在设备树文件中添加此属性,表示适用于该设备数绑定描述的规则。include 表示引用。

通过 include 字段可以发现,设备树绑定文件是存在继承关系的,即该设备树绑定文件继承了 st,stm32-spi-common.yaml 中描述的规则。拥有该属性的设备树节点也需要满足其父文件规定的规则。

可以利用 Ctrl + Click 打开其父文件。其父文件 st,stm32-spi-common.yaml 的内容如下所示。

include: [spi-controller.yaml, pinctrl-device.yaml]

properties:
  reg:
    required: true

  interrupts:
    required: true

  pinctrl-0:
    required: true

  pinctrl-names:
    required: true

  ioswp:
    type: boolean
    description: Swap SPI MOSI and MISO pins

# ...

properties 字段表示属性,是满足该设备数绑定文件 compatible 字段的设备树可以(必须)添加的属性列表。如上图所示,reg interrupts pinctrl-0 等字段是必须填写的。所以在设备树中,需要写:

spi5: spi@40015000 {
	compatible = "st,stm32-spi";
	#address-cells = <1>;
	#size-cells = <0>;
	reg = <0x40015000 0x400>;
	clocks = <&rcc STM32_CLOCK(APB2, 20)>;
	interrupts = <85 0>;
	st,spi-data-width = "limited-8-16-bit";
	status = "disabled";
};

同时,其还 include 了 spi-controller.yaml,内容包含了:

properties:
  clock-frequency:
    type: int
    description: |
      Clock frequency the SPI peripheral is being driven at, in Hz.
  "#address-cells":
    required: true
    const: 1
  "#size-cells":
    required: true
    const: 0
  cs-gpios:
    type: phandle-array
    description: |
      An array of chip select GPIOs to use. Each element
      in the array specifies a GPIO. The index in the array
      corresponds to the child node that the CS gpio controls.
      
# ...

上面的设备树文件是从 stm32f411.dts 中截取的。可以发现,ST 作为 Zephyr 的赞助商已经为我们定义好了绝大部分设备树节点。在大部分情况下,我们仅需要在 overlay 文件中对其进行少许处理,比如调整其 status 字段为 "okay" 等。此外,还可以用 cs-gpios 字段指定设备等。

需要注意的是,在 yaml 文件中,字段 pinctrl-0 是必须定义(required)的,但是 st 官方缺并没有填写这一字段。这就需要我们在 overlay 中填写这个字段,实现引脚复用。

Pin Controller

Zephyr 通过一系列 pinctrl 文件来实现引脚复用。比如对于 STM32F411 芯片,其在文件 modules/hal/stm32/dts/st/f4/stm32f411r(c-e)tx-pinctrl.dtsi 中定义了相关的引脚信息。截取一部分:

# ...

/omit-if-no-ref/ spi5_miso_pa12: spi5_miso_pa12 {
		pinmux = <STM32_PINMUX('A', 12, AF6)>;
		bias-pull-down;
		slew-rate = "very-high-speed";
	};

	/* SPI_MOSI */

	/omit-if-no-ref/ spi1_mosi_pa7: spi1_mosi_pa7 {
		pinmux = <STM32_PINMUX('A', 7, AF5)>;
		bias-pull-down;
		slew-rate = "very-high-speed";
	};
	
# ...

可以发现,其将芯片上所有的引脚都进行了定义,绑定到了对应的外设上,命名方式为 <外设名称>_<引脚名称>_<物理引脚编号>。通过这种方式,我们可以很方便的指定某个引脚的复用信息。

Overlay

通过以上的内容,我们就可以很容易地编写 overlay 文件:

&spi3 {
	status = "okay";
	pinctrl-0 = <&spi3_sck_pb12 &spi3_miso_pc11 &spi3_mosi_pc12>;
	pinctrl-names = "default";
	cs-gpios = <&gpioa 4 GPIO_ACTIVE_LOW>;

	spi-device@0 {
		reg = <0>;
	};
};

其中只包含了引脚复用和开启/关闭信息以及 SPI 从设备信息,易于编写。

代码使用

Zephyr 通过设备模型使得所有类型的 SoC 和板卡都可以通过统一的 API 进行外设访问。了解如何访问有两种方式,一种是利用官方文档的 API 说明,这种方法显然比较低效。

另一种比较高效的方式是查看 samples 示例。Zephyr 在 zephyr/samples/drivers 中提供了对于绝大部分外设的使用示例,以 spi 外设 spi_fujitsu_fram 为例:

#include <zephyr/drivers/spi.h>

// ...

static uint8_t data[MAX_USER_DATA_LENGTH], cmp_data[MAX_USER_DATA_LENGTH];

static int mb85rs64v_access(const struct device *spi,
			    struct spi_config *spi_cfg,
			    uint8_t cmd, uint16_t addr, void *data, size_t len)
{
	uint8_t access[3];
	struct spi_buf bufs[] = {
		{
			.buf = access,
		},
		{
			.buf = data,
			.len = len
		}
	};
	struct spi_buf_set tx = {
		.buffers = bufs
	};

	access[0] = cmd;

	if (cmd == MB85RS64V_WRITE_CMD || cmd == MB85RS64V_READ_CMD) {
		access[1] = (addr >> 8) & 0xFF;
		access[2] = addr & 0xFF;

		bufs[0].len = 3;
		tx.count = 2;

		if (cmd == MB85RS64V_READ_CMD) {
			struct spi_buf_set rx = {
				.buffers = bufs,
				.count = 2
			};

			return spi_transceive(spi, spi_cfg, &tx, &rx);
		}
	} else {
		tx.count = 1;
	}

	return spi_write(spi, spi_cfg, &tx);
}

int main() {
	// ...
	
	int err = mb85rs64v_access(spi, spi_cfg,
			       MB85RS64V_WRITE_ENABLE_CMD, 0, NULL, 0);
	// ...
}

这样,就可以打通从外设配置到代码调用的过程了。

Last modified: 2026-05-24